package com.derbysoft.lowcostadapter.dswitch3.service.impl;

import com.derbysoft.dswitch.dto.common.KeyValue;
import com.derbysoft.dswitch.dto.hotel.avail.AvailRoomStayDTO;
import com.derbysoft.dswitch.dto.hotel.common.ChargeItemDTO;
import com.derbysoft.dswitch.dto.hotel.common.RateDTO;
import com.derbysoft.dswitch.dto.hotel.common.StayDateRangeDTO;
import com.derbysoft.dswitch.dto.hotel.common.TotalDTO;
import com.derbysoft.dswitch.dto.hotel.reservation.ReservationRoomStayDTO;
import com.derbysoft.dswitch.remote.hotel.dto.*;
import com.derbysoft.lowcostadapter.commons.dto.Book;
import com.derbysoft.lowcostadapter.commons.dto.BookResponse;
import com.derbysoft.lowcostadapter.commons.dto.Payment;
import com.derbysoft.lowcostadapter.commons.dto.ReturnStatus;
import com.derbysoft.lowcostadapter.commons.exception.*;
import com.derbysoft.lowcostadapter.commons.model.Channel;
import com.derbysoft.lowcostadapter.commons.model.Hotel;
import com.derbysoft.lowcostadapter.commons.model.RateCache;
import com.derbysoft.lowcostadapter.commons.redis.service.ChannelCacheService;
import com.derbysoft.lowcostadapter.commons.redis.service.RateCacheService;
import com.derbysoft.lowcostadapter.commons.redis.utils.DateUtils;
import com.derbysoft.lowcostadapter.commons.redis.utils.KeyValueHelper;
import com.derbysoft.lowcostadapter.commons.repository.ChannelRepository;
import com.derbysoft.lowcostadapter.commons.repository.ReservationRepository;
import com.derbysoft.lowcostadapter.commons.utils.DateTimeUtils;
import com.derbysoft.lowcostadapter.commons.utils.ErrorMessageUtils;
import com.derbysoft.lowcostadapter.dswitch3.handler.impl.ElapsedTimeType;
import com.derbysoft.lowcostadapter.dswitch3.service.HotelProviderService;
import com.derbysoft.lowcostadapter.dswitch3.translate.Translator;
import com.derbysoft.lowcostadapter.webservice.LowCostWebService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StopWatch;

import java.math.BigDecimal;
import java.util.List;

import static com.derbysoft.lowcostadapter.dswitch3.service.impl.PaymentHelper.translatePayment;

/**
 * Created by: jason
 * Date: 2012-06-27
 */
@Component("ds3HotelReservationService")
public class HotelReservationServiceImpl extends BaseHotelService
        implements HotelProviderService<HotelReservationRequest, HotelReservationResponse> {

    private static Logger logger = LoggerFactory.getLogger(HotelReservationServiceImpl.class);

    @Autowired
    private ChannelCacheService channelCacheService;

    @Autowired
    private ChannelRepository channelRepository;

    @Override
    public HotelReservationResponse execute(HotelReservationRequest request) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        try {
            Channel channel = loadChannel(request.getHeader().getSource());
            saveReservation(request);
            BookResponse bookResponse;
            if (channel.getNormalMakeReservation()) {
                HotelAvailResponse hotelAvailResponse = getAvailability(request);
                if (isNoAllotment(hotelAvailResponse)) {
                    return createNoAllotmentResponse(request, stopWatch);
                }
                bookResponse = makeReservation(request, hotelAvailResponse);
            } else {
                bookResponse = makeReservationDirectly(request);
            }
            if (stopWatch.isRunning()) {
                stopWatch.stop();
            }
            long elapsedTime = stopWatch.getTotalTimeMillis();

            saveReservationNumber(request.getHotelReservationRQ().getErsp(), bookResponse);

            HotelReservationResponse reservationResponse = ds3ReservationRSTranslator.translate(bookResponse, request);

            reservationResponse.getElapsedTimes().add(new KeyValue(ElapsedTimeType.LCB_RESERVATION.name(), String.valueOf(elapsedTime)));

            return reservationResponse;
        } catch (Exception ex) {
            logger.error(ErrorMessageUtils.buildErrorMessage(ex));
            if (stopWatch.isRunning()) {
                stopWatch.stop();
            }
            HotelReservationResponse failedResponse = createErrorResponse(request, ex);
            failedResponse.getElapsedTimes().add(new KeyValue(ElapsedTimeType.LCB_RESERVATION.name(), String.valueOf(stopWatch.getTotalTimeMillis())));
            return failedResponse;
        }
    }

    private HotelReservationResponse createNoAllotmentResponse(HotelReservationRequest request, StopWatch stopWatch) {
        stopWatch.stop();
        long elapsedTime = stopWatch.getTotalTimeMillis();
        removeRates(request);
        return createNoAllotment(request, elapsedTime);
    }

    private boolean isNoAllotment(HotelAvailResponse hotelAvailResponse) {
        return hotelAvailResponse.getError() != null
                || CollectionUtils.isEmpty(hotelAvailResponse.getHotelAvailRS().getHotelAvailRoomStaysList())
                || CollectionUtils.isEmpty(hotelAvailResponse.getHotelAvailRS().getHotelAvailRoomStaysList().get(0).getRoomStaysList());
    }

    private BookResponse makeReservationDirectly(HotelReservationRequest request) throws LCBInvocationException, MakeReservationException {
        return (BookResponse) reservationWebService.send(createBook(request));
    }

    private BookResponse makeReservation(HotelReservationRequest reservationRequest, HotelAvailResponse hotelAvailResponse) throws LCBInvocationException {
        return (BookResponse) reservationWebService.send(createBook(reservationRequest, hotelAvailResponse));
    }

    private Book createBook(HotelReservationRequest reservationRequest) throws MakeReservationException {
        Book book = ds3ReservationRQTranslator.translate(reservationRequest, null);
        ReservationRoomStayDTO roomStay = reservationRequest.getHotelReservationRQ()
                .getHotelReservation().getRoomStaysList().get(0);
        TotalDTO totalDTO = roomStay.getTotal();
        if (totalDTO.getAmountAfterTax() != null) {
            BigDecimal expectedTotal = calcExceptedTotal(roomStay, totalDTO);
            expectedTotal = expectedTotal.multiply(BigDecimal.valueOf(roomStay.getNumberOfUnits()));
            book.getBookRequest().getBookingDetails().setExpectedTotal(expectedTotal);
            Payment payment = translatePayment(book.getBookRequest(), reservationRequest);
            if (payment != null) {
                book.getBookRequest().setPayment(payment);
            }
            return book;
        }
        if (totalDTO.getAmountBeforeTax() == null) {
            throw new MakeReservationException("both AmountAfterTax and AmountBeforeTax is null !!");
        }
        BigDecimal expectedTotal = calcExceptedTotal(totalDTO);
        expectedTotal = expectedTotal.multiply(BigDecimal.valueOf(roomStay.getNumberOfUnits()));
        book.getBookRequest().getBookingDetails().setExpectedTotal(expectedTotal);
        Payment payment = translatePayment(book.getBookRequest(), reservationRequest);
        if (payment != null) {
            book.getBookRequest().setPayment(payment);
        }
        return book;
    }

    private BigDecimal calcExceptedTotal(TotalDTO totalDTO) {
        BigDecimal expectedTotal = BigDecimal.valueOf(totalDTO.getAmountBeforeTax());
        if (!CollectionUtils.isEmpty(totalDTO.getTaxesList())) {
            for (ChargeItemDTO chargeItemDTO : totalDTO.getTaxesList()) {
                expectedTotal = expectedTotal.add(BigDecimal.valueOf(chargeItemDTO.getValue()));
            }
        }
        if (!CollectionUtils.isEmpty(totalDTO.getServiceChargesList())) {
            for (ChargeItemDTO chargeItemDTO : totalDTO.getServiceChargesList()) {
                expectedTotal = expectedTotal.add(BigDecimal.valueOf(chargeItemDTO.getValue()));
            }
        }
        return expectedTotal;
    }

    private BigDecimal calcExceptedTotal(ReservationRoomStayDTO roomStay, TotalDTO totalDTO) {
        return BigDecimal.valueOf(totalDTO.getAmountAfterTax()).multiply(BigDecimal.valueOf(roomStay.getNumberOfUnits()));
    }

    private Channel loadChannel(String channelCode) {
        Channel channel = channelCacheService.get(channelCode);
        if (channel == null) {
            channel = channelRepository.load(channelCode);
        }
        if (channel == null) {
            throw new ChannelNotFoundException(channelCode);
        }
        return channel;
    }

    private void removeRates(HotelReservationRequest reservationRequest) {
        ReservationRoomStayDTO reservationRoomStayDTO = reservationRequest.getHotelReservationRQ().getHotelReservation().getRoomStaysList().get(0);

        Hotel hotel = loadHotel(reservationRequest.getHotelReservationRQ().getHotelReservation().getHotelCode());
        int adults = reservationRoomStayDTO.getAdultCount();
        int children = reservationRoomStayDTO.getChildCount();
        String ratePlan = reservationRoomStayDTO.getRatePlanCode();
        String roomTypeId = reservationRoomStayDTO.getRoomTypeCode();
        String field = ratePlan + KeyValueHelper.SEPARATOR + roomTypeId;
        StayDateRangeDTO stayDateRangeDTO = reservationRoomStayDTO.getStayDateRange();
        int los = DateTimeUtils.getIntervalDays(stayDateRangeDTO.getCheckin(), stayDateRangeDTO.getCheckout());

        for (int i = 0; i < los; i++) {
            String key = RateCache.PREFIX + KeyValueHelper.SEPARATOR + hotel.getPropertyID()
                    + KeyValueHelper.SEPARATOR + DateTimeUtils.formatDate(DateUtils.addDays(DateTimeUtils.parse(stayDateRangeDTO.getCheckin()), i))
                    + KeyValueHelper.SEPARATOR + adults
                    + KeyValueHelper.SEPARATOR + children;

            RateCache beforeRemove = rateCacheService.load(key, ratePlan, roomTypeId);
            logger.info(String.format("begin remove, key:[%s], field:[%s], price:[%s]", key, field, beforeRemove == null ? "NULL" : beforeRemove.getRate().toString()));

            rateCacheService.remove(key, ratePlan, roomTypeId);

            RateCache afterRemove = rateCacheService.load(key, ratePlan, roomTypeId);
            logger.info(String.format("after remove, key:[%s], field:[%s], price:[%s]", key, field, afterRemove == null ? "NULL" : afterRemove.getRate().toString()));
        }
    }

    private HotelReservationResponse createNoAllotment(HotelReservationRequest reservationRequest, long elapsedTime) {
        HotelReservationResponse noAllotmentResponse = noAllotmentResponse(reservationRequest);
        noAllotmentResponse.getElapsedTimes().add(new KeyValue(ElapsedTimeType.LCB_BEFORE_RESERVATION_AVAIL.name(), String.valueOf(elapsedTime)));
        return noAllotmentResponse;
    }

    private HotelAvailResponse getAvailability(HotelReservationRequest reservationRequest) {
        HotelAvailRequest hotelAvailRequest = reservationToAvailTranslator.translate(reservationRequest, null);
        return ds3HotelAvailabilityService.execute(hotelAvailRequest);
    }

    private void saveReservation(HotelReservationRequest reservationRequest) {
        reservationRepository.create(
                reservationRequest.getHotelReservationRQ().getHotelReservation().getHotelCode(),
                reservationRequest.getHotelReservationRQ().getErsp());
    }

    private Book createBook(HotelReservationRequest reservationRequest, HotelAvailResponse hotelAvailResponse) {
        Book book = ds3ReservationRQTranslator.translate(reservationRequest, null);
        ReservationRoomStayDTO roomStay = reservationRequest.getHotelReservationRQ()
                .getHotelReservation().getRoomStaysList().get(0);
        BigDecimal expectedTotal = calcExpectedTotal(roomStay, hotelAvailResponse);
        expectedTotal = expectedTotal.multiply(BigDecimal.valueOf(roomStay.getNumberOfUnits()));
        book.getBookRequest().getBookingDetails().setExpectedTotal(expectedTotal);
        return book;
    }

    private BigDecimal calcExpectedTotal(ReservationRoomStayDTO reservationRoomStayDTO, HotelAvailResponse hotelAvailResponse) {
        BigDecimal total = BigDecimal.ZERO;
        String ratePlanCode = reservationRoomStayDTO.getRatePlanCode();
        String roomTypeCode = reservationRoomStayDTO.getRoomTypeCode();
        int numberOfUnit = reservationRoomStayDTO.getNumberOfUnits();
        AvailRoomStayDTO availRoomStayDTO = findRoomStay(ratePlanCode, roomTypeCode, numberOfUnit,
                hotelAvailResponse.getHotelAvailRS().getHotelAvailRoomStaysList().get(0).getRoomStaysList());

        for (RateDTO rateDTO : availRoomStayDTO.getRoomRate().getRatesList()) {
            int intervalDays = DateTimeUtils.getIntervalDays(rateDTO.getDateRange().getStart(), rateDTO.getDateRange().getEnd()) + 1;
            total = total.add(BigDecimal.valueOf(rateDTO.getAmountAfterTax()).multiply(BigDecimal.valueOf(intervalDays)));
        }
        return total;
    }

    private AvailRoomStayDTO findRoomStay(String ratePlanCode, String roomTypeCode, int numberOfUnit, List<AvailRoomStayDTO> roomStays) {
        for (AvailRoomStayDTO roomStay : roomStays) {
            if (roomStay.getRatePlan().getCode().equals(ratePlanCode) && roomStay.getRoomType().getCode().equals(roomTypeCode)) {
                return roomStay;
            }
        }
        throw new NoAllotmentException(ratePlanCode, roomTypeCode, numberOfUnit);
    }

    private HotelReservationResponse noAllotmentResponse(HotelReservationRequest reservationRequest) {
        HotelReservationResponse reservationResponse = new HotelReservationResponse();
        reservationResponse.setTaskId(reservationRequest.getHeader().getTaskId());
        ErrorDTO error = new ErrorDTO();
        error.setCode(SYSTEM);
        error.setSource(adapter);
        String ratePlanCode = reservationRequest.getHotelReservationRQ().getHotelReservation().getRoomStaysList().get(0).getRatePlanCode();
        String roomTypeCode = reservationRequest.getHotelReservationRQ().getHotelReservation().getRoomStaysList().get(0).getRoomTypeCode();
        int numberOfUnit = reservationRequest.getHotelReservationRQ().getHotelReservation().getRoomStaysList().get(0).getNumberOfUnits();
        error.setMessage(String.format("No allotment for ratePlanCode[%s],roomTypeCode[%s],numberOfUnit[%d]", ratePlanCode, roomTypeCode, numberOfUnit));
        reservationResponse.setError(error);
        return reservationResponse;
    }

    private void saveReservationNumber(String ersp, BookResponse bookResponse) {
        ReturnStatus returnStatus = bookResponse.getBookResult().getReturnStatus();
        if (returnStatus.isSuccess() && bookResponse.getBookResult().getBookingReference() != null) {
            reservationRepository.updateBookingReference(ersp, bookResponse.getBookResult().getBookingReference());
        }
    }

    private HotelReservationResponse createErrorResponse(HotelReservationRequest reservationRequest, Exception ex) {
        HotelReservationResponse reservationResponse = new HotelReservationResponse();
        reservationResponse.setTaskId(reservationRequest.getHeader().getTaskId());
        ErrorDTO errorDTO = new ErrorDTO();
        errorDTO.setCode(SYSTEM);
        errorDTO.setSource(ex instanceof NetErrorException ? provider : adapter);
        errorDTO.setMessage(ErrorMessageUtils.buildErrorMessage(ex));
        reservationResponse.setError(errorDTO);
        return reservationResponse;
    }

    @Autowired
    @Qualifier("reservationToAvailTranslator")
    private Translator<HotelReservationRequest, HotelAvailRequest, Void> reservationToAvailTranslator;

    @Autowired
    @Qualifier("ds3GetAvailService")
    private HotelProviderService<HotelAvailRequest, HotelAvailResponse> ds3HotelAvailabilityService;

    @Autowired
    @Qualifier("ds3ReservationRQTranslator")
    private Translator<HotelReservationRequest, Book, Void> ds3ReservationRQTranslator;

    @Autowired
    @Qualifier("ds3ReservationRSTranslator")
    private Translator<BookResponse, HotelReservationResponse, HotelReservationRequest> ds3ReservationRSTranslator;

    @Autowired
    @Qualifier("reservationWebService")
    private LowCostWebService reservationWebService;

    @Autowired
    private ReservationRepository reservationRepository;

    @Autowired
    private RateCacheService rateCacheService;
}
